feat(tracker): Linear adapter#43
Open
harshitsinghbhandari wants to merge 2 commits into
Open
Conversation
Read-only v1 adapter against Linear's GraphQL API, mirroring the
github adapter's file layout, sentinel errors, RateLimitError shape,
and atomic.Bool+sync.Mutex preflight caching. Hand-rolled GraphQL
over net/http — no Linear SDK dependency.
Issue identity: TrackerID.Native is opaque (short id or UUID) and
passes straight to issue(id:). TrackerRepo.Native is the team key;
List resolves it lazily to a team UUID via teams(filter:{key:{eq}})
and caches the mapping. Empty Native means workspace-wide list.
Authorization header is sent raw — no Bearer prefix — because v1
only supports personal API keys.
Errors are classified via errors[].extensions.type (Linear's
lowercase-words discriminator) with HTTP status as fallback;
ratelimited surfaces RateLimitError with RetryAfter and ResetAt
parsed from Retry-After / X-RateLimit-Requests-Reset.
State map: completed→done, canceled→cancelled, started→in_progress,
unstarted/triage/backlog/unknown→open. NO `review` in v1 — doc.go
explains why.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Linear has no CLI keyring like gh, so a personal API key in an env
var is the only path. Make the failure mode self-fixing:
* ErrNoToken's message names both the LINEAR_API_KEY env var and
https://linear.app/settings/api, so a fresh dev hitting it sees
the fix in the error itself.
* EnvTokenSource wraps the sentinel with the deduped list of env
vars it actually consulted, so multi-env setups (e.g. project
overrides) surface exactly which names matter.
* doc.go gains a Getting started section pointing at the same URL.
errors.Is(err, ErrNoToken) still routes — SM matching is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3 tasks
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Read-only Linear adapter for the existing
ports.Trackerinterface — Get, List, Preflight. Mirrors the github adapter file layout (tracker.go,auth.go,doc.go,tracker_test.go), shares the same sentinel error surface (ErrNotFound,ErrAuthFailed,ErrRateLimited,ErrWrongProvider,ErrBadID,ErrNoToken), the same typedRateLimitError{ResetAt, RetryAfter}, and the same atomic.Bool + sync.Mutex preflight caching.Do NOT merge yet — waiting on PR #40 (Comment/Transition) to ensure the write side is shaped consistently before this lands.
Why hand-rolled GraphQL (no SDK)
@linear/sdkis a TypeScript artifact; equivalent Go ports ship a ~700KB+ generated documents file we'd touch ~3 endpoints of.httptestthat inspects{query, variables}— no SDK shim required.extensions.type→ sentinel) with the same contract surface as the github adapter.Auth header — NO Bearer prefix
Linear personal API keys are sent raw:
OAuth tokens DO use Bearer, but v1 only supports personal keys. This is the single easiest bug to introduce —
TestAuthHeader_NoBearerPrefixpins it.State mapping (Linear
state.type→NormalizedIssueState)state.typecompleteddonecanceledcancelledstartedin_progressunstartedopentriageopenbacklogopenopenreviewis intentionally not produced. Linear has no native review type; teams using "In Review" settype=started, which collapses toin_progress. Name-based mapping is brittle across customized workflows — seedoc.gofor the full rationale.Errors
Linear surfaces failures via
errors[].extensions.type(lowercase strings, e.g."authentication error","ratelimited","feature not accessible","forbidden") — NOTextensions.codewithSCREAMING_SNAKE_CASE. This was the one place the implementation diverged from the task prompt; the authoritative source is@linear/sdk'serror.tsat HEAD (lowercase strings) and the adapter codes to that reality.doc.gocalls this out explicitly.Classification priority:
extensions.type→ sentinel (handles 200 + ratelimited / authentication error)errors[]on a 2xx → generic graphql error with original messageType normalization is case-insensitive (
strings.ToLower(TrimSpace(...))) to defend against upstream casing drift.Links
Test plan
go test ./... -race -count=1(343 tests, 15 packages, all green)go vet ./...cleangofmt -l backend/internal/adapters/tracker/linear/cleanhttptest.NewServerrouting by parsed{query, variables}body — no mocks oft.dosuperpowers:code-reviewer; addressed all strong-recommends (team-cache mutex no longer held across network IO; case-insensitiveextensions.typematching; Preflight token-rotation behavior documented)🤖 Generated with Claude Code